//
// Server.as
//
// Copyright (c) 2009 Nathan Barnes
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//
// The server agent for SimpleAVRG

package {

import com.threerings.util.HashSet;
import com.threerings.util.Log;
import com.threerings.util.Set;
import com.whirled.ServerObject;
import com.whirled.avrg.AVRServerGameControl;
import com.whirled.avrg.AVRGameControlEvent;
import com.whirled.avrg.AVRGamePlayerEvent;
import com.whirled.avrg.AVRGameRoomEvent;
import com.whirled.avrg.RoomSubControlServer;
import com.whirled.net.MessageReceivedEvent;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Dictionary;
import flash.utils.Timer;

/**
 * The server agent for SimpleAVRG. Automatically created by the 
 * whirled server whenever a new game is started. 
 */
public class Server extends ServerObject
{
    /** 
     * This is the interval at which the game decides whether or not to add
     * monsters and the odds that it will add a monster for each player it
     * considers.
     */
    public const ADD_MONSTER_DELAY :int = 1000;

    public const ADD_MONSTER_CHANCE :Number = 0.05;

    /**
     * Constructs a new server agent.
     */
    public function Server ()
    {
        _log.info("SimpleAVRG server agent reporting for duty!");

        _control = new AVRServerGameControl(this);

        // listen for an unload event
        _control.addEventListener(Event.UNLOAD, handleUnload);

        // listen for players joining and quitting the game
        _control.game.addEventListener(AVRGameControlEvent.PLAYER_JOINED_GAME,
            handlePlayerJoinedGame);
        _control.game.addEventListener(AVRGameControlEvent.PLAYER_QUIT_GAME,
            handlePlayerQuitGame);

        // start with no monsters
        _numMonsters = 0;
        _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, 0);
         
        // This dictionary will store the count of monsters in each room.
        _roomMonsters = new Dictionary();

        // This dictionary will store the count of monsters in each player's basket.
        _basketMonsters = new Dictionary();

        // add monsters periodically
        _monsterTimer = new Timer(ADD_MONSTER_DELAY);
        _monsterTimer.addEventListener(TimerEvent.TIMER, maybeAddMonsters);
        _monsterTimer.start();

        // listen for game-level messages
        _control.game.addEventListener(MessageReceivedEvent.MESSAGE_RECEIVED,
            handleGameMessageReceived);
    }

    // Consider adding monsters.
    protected function maybeAddMonsters (event :TimerEvent) :void
    {
        // For each player in the game, consider adding a monster to that
        // player's location. This means that rooms with more players in them
        // have greater odds of receiving monsters.
        for each (var playerId :int in _control.game.getPlayerIds()) {
            if (Math.random() <= ADD_MONSTER_CHANCE) {
                addMonster( _control.getPlayer(playerId).getRoomId() );
            }
        }
    }

    // Add a monster to a room and broadcast its addition.
    // This should only be called for a room that is known to contain at least
    // one player.
    protected function addMonster (roomId :int) :void
    {
        // Add a monster to the room.
        var room :RoomSubControlServer = _control.getRoom(roomId);

        var mobId :String = String(_nextMobId);

        _log.info("Adding monster #" + mobId + " to room #" + roomId);
        if (!_roomMonsters[roomId].contains(_nextMobId)) {

            // Create the mob at a random location, but on the floor.
            room.spawnMob(mobId, MonsterMob.GENERIC_MONSTER, Math.random(), 0, Math.random());
            _nextMobId += 1;

        } else {
            _log.error("Attempted to add monster #" + mobId + " to room #" + roomId 
                     + ", but the monster was already there!");
        }
    }

    // Remove a number of monsters from the game and report the removal.
    protected function removeGlobalMonsters (removedMonsters :int) :void
    {
        _log.info("Removing " + removedMonsters + " monsters from global count.");
        _numMonsters -= removedMonsters;
        _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, _numMonsters);
    }

    // Set up a new room for players who have bravely gone where no player has gone before
    // ...or at least where no other player was at the moment.
    protected function setupRoom (roomId :int) :void
    {
        var room :RoomSubControlServer = _control.getRoom(roomId);
        room.addEventListener(AVRGameRoomEvent.ROOM_UNLOADED, handleRoomUnloaded);
        room.addEventListener(AVRGameRoomEvent.MOB_CONTROL_AVAILABLE, handleMobControlAvailable);

        _roomMonsters[roomId] = new HashSet();
    }

    // Monsters flee an empty room.
    // (Maybe later, we'll have them flee the empty room into another room
    // in the game. For now, they return to the Underwhirled.)
    protected function emptyRoom (roomId :int) :void
    {
        _log.info("Emptying room #" + roomId);
        if (_roomMonsters[roomId] != null) {
            // Remove the spawned mobs from the room.
            var room :RoomSubControlServer = _control.getRoom(roomId);
            room.getSpawnedMobs().forEach( function (item:*, index:int, array:Array) :void {
                var mobId :String = String(item);
                room.despawnMob(mobId);
            });

            // Update the room property count.
            room.props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT, 0);

            // Remove the monsters from the local dictionary.
            var removedMonsters :int = _roomMonsters[roomId].size();
            delete _roomMonsters[roomId];

            // Remove the monsters from the global count.
            removeGlobalMonsters(removedMonsters);
        } else {
            _log.info("No monsters to remove from room #" + roomId);
        }
    }

    // This is called when a mob has been spawned in a room.
    protected function handleMobControlAvailable (event :AVRGameRoomEvent) :void
    {
        // Add the monster id to the local dictionary of monsters in rooms.
        var mobId :String = event.name;
        _roomMonsters[event.roomId].add(mobId);

        // Set the room-level count.
        _control.getRoom(event.roomId).props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT,
            _roomMonsters[event.roomId].size());

        // Store the sum so we don't have to recalculate it.
        _numMonsters += 1;

        // Transmit the new global total.
        _control.game.props.set(SimpleAVRGConstants.GLOBAL_MONSTER_COUNT, _numMonsters);
    }

    // This is called when a player joins the game.
    protected function handlePlayerJoinedGame (event :AVRGameControlEvent) :void
    {
        // Retrieve the player id from the event.
        var playerId :int = int(event.value);

        // Give the player a basket.
        _basketMonsters[playerId] = 0;

        // Listen for the player moving from room to room so that after the last
        // player leaves a room, the system can turn out the lights.
        _control.getPlayer(playerId).addEventListener(AVRGamePlayerEvent.ENTERED_ROOM,
            handlePlayerEnteredRoom);
        _control.getPlayer(playerId).addEventListener(AVRGamePlayerEvent.LEFT_ROOM,
            handlePlayerLeftRoom);
    }

    // This is called when a player enters a room.
    protected function handlePlayerEnteredRoom (event :AVRGamePlayerEvent) :void
    {
        var playerId :int = event.playerId;
        var roomId :int = int(event.value);
        
        // If the room isn't already set up, set it up now.
        if (_roomMonsters[roomId] == null) {
            setupRoom(roomId);
        }
    }

    // This is called when a player leaves a room (or the game).
    protected function handlePlayerLeftRoom (event :AVRGamePlayerEvent) :void
    {
        // Retrieve the player id and the room id from the event.
        var playerId :int = event.playerId;
        var roomId :int = int(event.value);

        // Check whether any players remain in the room.
        // TODO Confirm that this event is guaranteed to occur only after the player's id
        //      has been removed from the room's player list.
        // FIXME This probably sets up a race condition between the event handler and the room
        //       object getting dropped, but I don't see a better way to clean up the room before
        //       it is unloaded. The ROOM_UNLOADED event also races the actual memory cleanup, 
        //       and is even more likely to lose.
        if (_control.getRoom(roomId).getPlayerIds().length == 0) {
            emptyRoom(roomId);
        }
    }

    // This is called when a room is being unloaded.
    protected function handleRoomUnloaded (event :AVRGameRoomEvent) :void
    {
        var room :RoomSubControlServer = _control.getRoom(event.roomId);
        room.removeEventListener(AVRGameRoomEvent.ROOM_UNLOADED, handleRoomUnloaded);
        room.removeEventListener(AVRGameRoomEvent.MOB_CONTROL_AVAILABLE,
            handleMobControlAvailable);
    }

    // This is called when a player quits the game.
    protected function handlePlayerQuitGame (event :AVRGameControlEvent) :void
    {
        // Retrieve the player id from the event.
        var playerId :int = int(event.value);

        // Take the player's basket away.
        delete _basketMonsters[playerId];

        // Stop listening for the player to leave rooms.
        // TODO Check whether it's really necessary to explicitly remove this. It shouldn't be.
        _control.getPlayer(playerId).removeEventListener(AVRGamePlayerEvent.LEFT_ROOM,
            handlePlayerLeftRoom);
    }

    // This is called when a game message has been received.
    protected function handleGameMessageReceived (event :MessageReceivedEvent) :void
    {
        if (event.name == SimpleAVRGConstants.CATCH_MONSTER_REQUEST_MESSAGE) {
            // Find the player catching a monster and the room from which it is being caught.
            var playerId :int = event.senderId;
            var roomId :int = _control.getPlayer(event.senderId).getRoomId();
            var mobId :String = String(event.value);

            _log.info("Player #" + playerId + " is attempting to take monster #" + mobId 
                    + " from room #" + roomId + ".");
            var monsters :Set = Set(_roomMonsters[roomId]);
            if (monsters != null && monsters.contains(mobId)) {
                // Remove the mob.
                _control.getRoom(roomId).despawnMob(mobId);

                // Set the room-level count.
                monsters.remove(mobId);
                _control.getRoom(roomId).props.set(SimpleAVRGConstants.ROOM_MONSTER_COUNT,
                    monsters.size());

                // Set the global count.
                removeGlobalMonsters(1);

                // Set the player's basket count.
                _basketMonsters[playerId] += 1;
                _control.getPlayer(playerId).props.set(SimpleAVRGConstants.BASKET_MONSTER_COUNT,
                    _basketMonsters[playerId]);
            } else {
                _log.warning("Player #" + playerId + " attempted to remove monster #" + mobId 
                           + " from room #" + roomId + ", but the monster wasn't there!");
            }
        }
    }

    // Don't pollute! Unload your resources.
    protected function handleUnload (event :Event) :void
    {
        if (_monsterTimer != null) {
            _monsterTimer.stop();
            _monsterTimer.removeEventListener(TimerEvent.TIMER, maybeAddMonsters);
        }
    }

    // A timer for periodically adding monsters.
    protected var _monsterTimer :Timer;

    // This is the total number of monsters in the whole game.
    protected var _numMonsters :int = 0;

    // A counter that provides unique mob ids.
    protected var _nextMobId :int = 0;

    // This is the number of monsters in each room.
    protected var _roomMonsters :Dictionary;

    // This is the number of monsters in each player's basket.
    protected var _basketMonsters :Dictionary;

    // The server side game control.
    protected var _control :AVRServerGameControl;

    // The logger.
    protected var _log :Log = Log.getLog(Server);
}

}
